The Spectrum of Components
So, here's an implementation of a <Banner />
component, meant to show the user a message:
Code Playground
Take a few moments to consider this component, and how it's structured. What do you think about it?
Let's discuss.
Video Summary
This code doesn't have any bugs, any errors... it isn't even deviating from the “best practices” we've seen throughout the first few modules of this course. There's nothing explicitly wrong with it, but there is a bit of a missed opportunity.
I'd like to introduce you to the spectrum of components.
On the left end of this spectrum, we have our generic, low-level, reusable LEGO brick components, things like <Button>
and <Modal>
. As we move up the spectrum, we get components like <SearchIconButton>
and <LoginForm>
. At the far end, our highest-level component is <App>
.
The further we move along this spectrum, the more and more "tied in" the components get to our application. For <LoginForm>
, for example, we need to know quite a bit about our user model: do users use email addresses or usernames to log in? For <UserProfileCard>
, we need to know if users have an avatar, and if so, what size it is.
Every component should have a clear spot on this spectrum. It should be clear if our component is a low-level LEGO brick, a high-level application-specific view, or something in-between.
Now, looking again at the code from above, we have a Banner
component... But where should we put it?
On the one hand, <Banner>
seems like a low-level thing, something we might find in a component library. So maybe it should go on the left end?
But, we also have some business logic stuff in this component: we check and see if we have a user, if that user's registrationStatus
property is equal to the string "unverified"
. That's not typically something we'd find in a low-level LEGO brick!
As a result, this component sorta “stretches out” along the spectrum:
We'll talk about why I think this is a bad thing shortly, but first, let me show you how I would structure this <Banner>
component:
function Banner({ type, children }) { const backgroundColor = type === 'success' ? 'var(--color-success)' : 'var(--color-error)';
return ( <div className={styles.banner} style={{ backgroundColor }} > {children} </div> );}
export function LoggedInBanner({ type, user, children,}) { if ( !user || user.registrationStatus === 'unverified' ) { return null; }
return <Banner type={type}>{children}</Banner>;}
export default Banner;
We've now split the responsibilities up into 2 components:
Banner
, a low-level LEGO brick that deals with the UI concerns, showing a bannerLoggedInBanner
, a mid-level component that checks whether the user is verified, and renders aBanner
if so.
We're composing the Banner
component, passing along the relevant components.
Here's what this looks like:
In terms of the benefits, there are two big ones.
The first is flexibility. Now we have these two separate components, and we can use them in different scenarios. If we need a generic banner to show all the time, we can use <Banner>
. If we want a banner that only shows for logged-in users, we can use <LoggedInBanner>
.
The second benefit, and honestly the more important one, is that it allows us to keep things simple even as our application grows in size and scale.
I used to work at Khan Academy, which was one of the first organizations outside of Facebook to really adopt React. It was 7-8 years old, with 30-40 developers working on it, and things got complex.
We would have components that seemed low-level, like banners or buttons, that would have 600+ lines of code. Absolutely full of business logic and special conditions and edge-cases. Every time the product team had a new requirement, we'd cram it all in the same component. It got overwhelming.
With this new model, we would keep spinning off new variations as product requirements come up.
Let's suppose we're having a Black Friday sale. Rather than adding a bunch of logic to the <Banner>
component, we could create a <BlackFridaySaleBanner>
:
function BlackFridaySaleBanner({ saleStartDate, saleEndDate }) { // Check to see if the banner should be shown const now = new Date(); if (now < saleStartDate || now > saleEndDate) { return null; }
// Hardcode the type and children return ( <Banner type="success"> We're having a Black Friday sale! Get 50% off selected products. </Banner> );}
All of these higher-level "spinoffs" still use the same underlying <Banner>
component. That way, if we have a design update (eg. tweaked padding, new colors), all of these spinoffs will automatically be updated as well.
I first learned of this mental model back in 2016, and it's really changed how I build React applications. I feel like I can keep things simple even as the product grows, which is a really rare feeling in software development!
Here's the final code from the video:
Code Playground